package org.example.system.service.impl.oss;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import io.minio.messages.Item;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.example.system.model.dto.FileInfo;
import org.example.system.model.dto.TreeFileList;
import org.example.system.model.dto.TreeListChildren;
import org.example.system.service.OssService;
import org.example.system.service.SysUserService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * MinIO 文件上传服务类
 *
 * @author LY
 */
@Component
@ConditionalOnProperty(value = "oss.type", havingValue = "minio")
@ConfigurationProperties(prefix = "oss.minio")
@RequiredArgsConstructor
@Data
public class MinioOssServiceImpl implements OssService {

    private final SysUserService sysUserService;

    /**
     * 服务Endpoint
     */
    private String endpoint;
    /**
     * 访问凭据
     */
    private String accessKey;
    /**
     * 凭据密钥
     */
    private String secretKey;
    /**
     * 存储桶名称
     */
    private String bucketName;
    /**
     * 自定义域名
     */
    private String customDomain;

    private MinioClient minioClient;

    // 依赖注入完成之后执行初始化
    @PostConstruct
    public void init() {
        minioClient = MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }

    /**
     * 文件上传
     *
     * @param file 表单文件对象
     * @return FileInfo
     */
    @Override
    public FileInfo uploadFile(MultipartFile file) {
        return upMinIo(file);
    }

    /**
     * 文件上传到 Minio 服务器
     *
     * @param file 需要上传的文件对象，类型为 {@link MultipartFile}
     * @return 返回文件上传后的信息，包含文件名和文件访问 URL，类型为 {@link FileInfo}
     * @throws RuntimeException 如果文件名为空或上传过程中发生异常，抛出运行时异常
     */
    private FileInfo upMinIo(MultipartFile file) {
        // 生成文件名(日期文件夹)
        String originalFilename = file.getOriginalFilename();
        if (StrUtil.isBlank(originalFilename)) {
            throw new RuntimeException("文件名不能为空");
        }
        // 文件名安全校验，防止路径遍历攻击
        String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9.-]", "_");
        // 文件名相同会进行覆盖
        String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + safeFilename;
        //  try-with-resource 语法糖自动释放流
        try (InputStream inputStream = file.getInputStream()) {
            // 文件上传
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .contentType(file.getContentType())
                    .stream(inputStream, file.getSize(), -1)
                    .build();
            minioClient.putObject(putObjectArgs);

            // 返回文件路径
            String fileUrl;
            // 未配置自定义域名
            if (StrUtil.isBlank(customDomain)) {
                fileUrl = getFileUrl(fileName);
            } else { // 配置自定义文件路径域名
                // 处理域名末尾可能存在的斜杠
                String domain = customDomain.endsWith("/") ? customDomain.substring(0, customDomain.length() - 1) : customDomain;
                fileUrl = domain + '/' + bucketName + "/" + fileName;
            }

            FileInfo fileInfo = new FileInfo();
            fileInfo.setName(fileName);
            fileInfo.setUrl(fileUrl);
            return fileInfo;
        } catch (Exception e) {
            throw new RuntimeException("文件上传失败");
        }
    }

    /**
     * 上传用户头像并更新数据库中的头像地址。
     * 该方法接收一个表单文件对象，将其上传到MinIO存储服务，并返回文件信息。
     * 如果上传成功，将更新数据库中当前用户的头像存储地址；如果更新失败，则抛出异常。
     *
     * @param file 表单文件对象，包含用户上传的头像文件
     * @return FileInfo 包含上传文件的基本信息，如文件URL等
     * @throws RuntimeException 如果更新数据库中的头像地址失败，则抛出此异常
     */
    @Override
    public FileInfo uploadAvatar(MultipartFile file) {
        FileInfo fileInfo = upMinIo(file);
        // 更新数据库中当前用户的头像存储地址
        boolean b = sysUserService.updateAvatarUrl(fileInfo.getUrl());

        if (b) {
            return fileInfo;
        } else {
            throw new RuntimeException("头像上传失败");
        }
    }

    /**
     * 文件下载
     * 该方法用于从MinIO存储桶中下载指定文件，并将其作为响应返回给客户端，触发浏览器下载。
     *
     * @param fileName 需要下载的文件名，该文件名应与MinIO存储桶中的对象名称一致。
     * @return 返回一个包含文件输入流的ResponseEntity对象，响应头已设置为触发浏览器下载。
     * @throws RuntimeException 如果文件下载过程中发生异常，将抛出RuntimeException，并包含异常信息。
     */
    @Override
    public ResponseEntity<InputStreamResource> downLoadFile(String fileName) {
        try {
            GetObjectArgs args = GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .build();
            InputStream inputStream = minioClient.getObject(args);

            // 设置响应头触发浏览器下载
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentDispositionFormData("attachment", fileName);

            return ResponseEntity.ok().headers(headers).body(new InputStreamResource(inputStream));
        } catch (Exception e) {
            throw new RuntimeException("文件下载失败: " + e.getMessage(), e);
        }
    }

    /**
     * 批量获取文件 URL 列表。
     * 该方法通过 MinIO 客户端列出指定存储桶中的所有对象，并为每个对象生成文件 URL。
     * 如果文件名为空或非法，则跳过该文件。
     * 最终返回一个包含文件信息的树形结构列表。
     *
     * @return 包含文件信息的树形结构列表 {@link TreeFileList}
     * @throws NoSuchAlgorithmException 如果生成文件 URL 时发生算法异常
     * @throws IOException 如果 I/O 操作失败
     * @throws InvalidKeyException 如果生成文件 URL 时密钥无效
     */
    @Override
    public List<TreeFileList> getFileList() throws NoSuchAlgorithmException, IOException, InvalidKeyException {
        Iterable<Result<Item>> results;
        List<FileInfo> fileUrlList = new ArrayList<>(1000); // 预分配容量提升性能
        ListObjectsArgs args = ListObjectsArgs.builder()
                .bucket(bucketName)
                .recursive(true)
                .build();
        try {
            // 列出桶中所有对象
            results = minioClient.listObjects(args);
            for (Result<Item> result : results) {
                Item item = result.get();
                String fileName = item.objectName();

                // 忽略非法文件名
                if (fileName == null || fileName.isEmpty()) {
                    continue;
                }

                String fileUrl = getFileUrl(fileName);
                // 设置文件名以及文件 url
                FileInfo fileInfo = new FileInfo();
                fileInfo.setName(fileName);
                fileInfo.setUrl(fileUrl);
                fileUrlList.add(fileInfo);
            }
        } catch (MinioException e) {
            throw new RuntimeException("获取文件列表失败", e); // 包含原始异常信息
        }

        return buildFileList(fileUrlList);
    }

    /**
     * 删除指定文件名的文件。
     * 该方法通过MinIO客户端从指定的存储桶中删除文件。
     * 如果删除过程中发生异常，将抛出运行时异常。
     *
     * @param fileName 要删除的文件名，不能为null或空字符串
     * @throws IOException 如果发生I/O错误
     * @throws NoSuchAlgorithmException 如果请求的加密算法不可用
     * @throws InvalidKeyException 如果提供的密钥无效
     * @throws RuntimeException 如果删除文件失败
     */
    @Override
    public void deleteFile(String fileName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .build());
        } catch (MinioException e) {
            throw new RuntimeException("删除文件失败");
        }
    }

    /**
     * 批量删除文件
     * 该方法接收一个文件名列表，并依次删除列表中的每个文件。如果删除过程中出现异常，
     * 会抛出相应的异常。
     *
     * @param fileNameList 包含待删除文件名的列表，列表中的每个元素为一个文件的名称
     * @throws IOException 如果在删除文件过程中发生I/O错误
     * @throws NoSuchAlgorithmException 如果在删除文件过程中涉及到的加密算法不可用
     * @throws InvalidKeyException 如果在删除文件过程中使用了无效的密钥
     */
    @Override
    public void deleteFileBatch(List<String> fileNameList) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        for (String fileName : fileNameList) {
            deleteFile(fileName);
        }
    }

    /**
     * 批量文件上传
     * 该方法接收一个文件列表，并将每个文件上传到MinIO存储服务中。上传完成后，返回包含每个文件上传信息的集合。
     *
     * @param files 需要上传的文件列表，每个文件为MultipartFile类型
     * @return 上传后的文件信息集合，每个文件信息为FileInfo类型
     */
    @Override
    public List<FileInfo> uploadFiles(List<MultipartFile> files) {
        List<FileInfo> fileInfoList = new ArrayList<>();
        for (MultipartFile file : files) {
            FileInfo fileInfo = upMinIo(file);
            fileInfoList.add(fileInfo);
        }

        return fileInfoList;
    }

    /**
     * 文件批量下载
     * 该方法用于将指定的文件列表打包成一个 ZIP 文件，并返回该 ZIP 文件的字节数组响应实体。
     * 如果文件名列表为空或为 null，将抛出 IllegalArgumentException 异常。
     * 该方法使用 MinIO 客户端从指定的存储桶中获取文件，并将这些文件逐个添加到 ZIP 输出流中。
     * 最终，生成的 ZIP 文件将通过 ResponseEntity 返回，供用户下载。
     *
     * @param fileNameList 需要下载的文件名列表，列表中的每个字符串表示一个文件名。
     * @return 包含 ZIP 文件字节数组的 ResponseEntity 对象，响应头中设置了文件下载的相关信息。
     * @throws IllegalArgumentException 如果文件名列表为空或为 null。
     * @throws RuntimeException 如果文件打包过程中发生 IO 异常。
     */
    @Override
    public ResponseEntity<byte[]> downLoadFileZip(List<String> fileNameList) {
        if (fileNameList == null || fileNameList.isEmpty()) {
            throw new IllegalArgumentException("文件名列表不能为空");
        }

        try (ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
             ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {

            byte[] buffer = new byte[8192];

            for (String fileName : fileNameList) {
                try {
                    GetObjectArgs args = GetObjectArgs.builder()
                            .bucket(bucketName)
                            .object(fileName)
                            .build();

                    try (InputStream inputStream = minioClient.getObject(args)) {
                        // 对文件名进行 UTF-8 编码处理，防止中文乱码
                        String encodedFileName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
                        ZipEntry zipEntry = new ZipEntry(encodedFileName);
                        zos.putNextEntry(zipEntry);
                        int len;
                        while ((len = inputStream.read(buffer)) > 0) {
                            zos.write(buffer, 0, len);
                        }
                        zos.closeEntry();
                    }
                } catch (Exception ignored) {
                }
            }

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentDispositionFormData("attachment", "file.zip");

            return ResponseEntity.ok()
                    .headers(headers)
                    .body(zipOutputStream.toByteArray());

        } catch (IOException e) {
            throw new RuntimeException("文件打包失败", e);
        }
    }

    /**
     * 查询 Minio 文件系统中文件的 URL。
     * 该方法通过 Minio 客户端生成一个预签名的 URL，用于访问指定文件。
     * 如果文件名为空或 null，将抛出 IllegalArgumentException。
     * 如果生成的 URL 为空，将抛出 RuntimeException。
     * 生成的 URL 将移除查询参数部分，以确保 URL 的安全性。
     *
     * @param fileName 文件名，不能为空或 null
     * @return 文件的 URL，不包含查询参数
     * @throws IllegalArgumentException 如果文件名为空或 null
     * @throws RuntimeException 如果获取到的 URL 为空，或者在生成 URL 过程中发生异常
     */
    private String getFileUrl(String fileName) {
        if (fileName == null || fileName.isEmpty()) {
            throw new IllegalArgumentException("文件名不能为空");
        }

        try {
            // 构建获取预签名 URL 的参数
            GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
                    .bucket(bucketName)
                    .object(fileName)
                    .method(Method.GET)
                    .build();

            // 获取预签名 URL
            String newFileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);

            if (newFileUrl == null || newFileUrl.isEmpty()) {
                throw new RuntimeException("获取到的文件 URL 为空");
            }

            // 移除 URL 中的查询参数部分，以确保 URL 的安全性
            int queryStringIndex = newFileUrl.indexOf("?");
            if (queryStringIndex != -1) {
                newFileUrl = newFileUrl.substring(0, queryStringIndex);
            }

            return newFileUrl;
        } catch (MinioException | IOException | NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("获取文件 url 失败", e);
        }
    }

    /**
     * 将 Minio 文件列表转换为前端需要的树形格式。
     * 该函数会遍历 Minio 文件列表，将文件和文件夹分别处理，并构建成树形结构。
     * 对于文件，直接添加到结果列表中；对于文件夹，会创建文件夹节点，并将子文件添加到文件夹的 children 中。
     *
     * @param minioFileList Minio 文件列表，包含文件的基本信息（如名称、URL等）
     * @return 格式化之后的树形结构数据，包含文件和文件夹的层次关系
     */
    private List<TreeFileList> buildFileList(List<FileInfo> minioFileList) {
        if (minioFileList == null || minioFileList.isEmpty()) {
            return new ArrayList<>();
        }

        List<TreeFileList> fileList = new ArrayList<>();
        // 用于存储文件夹路径与其子文件的映射关系
        Map<String, List<TreeListChildren>> folderMap = new HashMap<>();

        // 遍历 Minio 文件列表，处理每个文件或文件夹
        for (FileInfo file : minioFileList) {
            String name = file.getName();
            int isDirIndex = name.indexOf("/");
            if (isDirIndex == -1) {
                // 处理文件：创建文件节点并添加到结果列表中
                TreeFileList treeFile = new TreeFileList();
                treeFile.setId(UUID.randomUUID().toString());
                treeFile.setName(name);
                treeFile.setUrl(file.getUrl());
                treeFile.setDir(false);
                treeFile.setFullPath(name);
                fileList.add(treeFile);
            } else {
                // 处理文件夹：提取文件夹路径和文件名，创建文件夹节点并维护子文件列表
                String folderPath = normalizePath(name.substring(0, isDirIndex));
                String fileName = name.substring(isDirIndex + 1);

                // 如果文件夹节点尚未创建，则创建并添加到结果列表中
                if (!folderMap.containsKey(folderPath)) {
                    TreeFileList treeFile = new TreeFileList();
                    treeFile.setId(UUID.randomUUID().toString());
                    treeFile.setName(getLastPathComponent(folderPath));
                    treeFile.setUrl("");
                    treeFile.setDir(true);
                    treeFile.setChildren(new ArrayList<>());
                    fileList.add(treeFile);

                    folderMap.put(folderPath, new ArrayList<>());
                }

                // 创建子文件节点并添加到对应文件夹的子文件列表中
                TreeListChildren childNode = new TreeListChildren();
                childNode.setName(fileName);
                childNode.setUrl(file.getUrl());
                childNode.setFullPath(name);
                childNode.setParentPath(folderPath);
                folderMap.get(folderPath).add(childNode);
            }
        }

        // 将子文件列表设置到对应的文件夹节点中
        for (TreeFileList file : fileList) {
            if (file.isDir()) {
                String folderName = file.getName();
                // 查找与文件夹名称匹配的路径，并设置其子文件列表
                for (Map.Entry<String, List<TreeListChildren>> entry : folderMap.entrySet()) {
                    if (entry.getKey().endsWith("/" + folderName) || entry.getKey().equals(folderName)) {
                        file.setChildren(entry.getValue());
                        break;
                    }
                }
            }
        }

        return fileList;
    }

    /**
     * 标准化路径，去除首尾斜杠并确保单层分隔
     *
     * @param path 需要标准化的路径字符串
     * @return 去除首尾斜杠后的路径字符串
     */
    private String normalizePath(String path) {
        // 使用正则表达式去除路径字符串的首尾斜杠
        return path.replaceAll("^/|/$", "");
    }

    /**
     * 获取路径的最后一级组件。
     * 该方法通过查找路径中最后一个斜杠（"/"）的位置，截取并返回斜杠之后的部分作为路径的最后一级组件。
     * 如果路径中不包含斜杠，则返回整个路径。
     *
     * @param path 输入的路径字符串，不能为null。
     * @return 路径的最后一级组件，如果路径中不包含斜杠，则返回整个路径。
     */
    private String getLastPathComponent(String path) {
        // 查找路径中最后一个斜杠的位置
        int lastSlash = path.lastIndexOf("/");
        // 根据斜杠的位置决定返回整个路径还是截取最后一级组件
        return lastSlash == -1 ? path : path.substring(lastSlash + 1);
    }
}
